package org.windblog.api.template;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.windblog.core.*;
import org.windblog.core.entity.*;

public class TemplateService {

	private static HttpServletRequest request;

	private static HttpServletResponse response;

	public static void init() {
		request = WBQuery.getRequest();
		response = WBQuery.getResponse();
	}

	/**
	 * Includes the header.jsp template file from your current theme's
	 * directory. if a name is specified then a specialised header
	 * header-{name}.jsp will be included.
	 * 
	 * @param name
	 *            header-{name}.jsp
	 * @return header
	 */
	public static String getHeader(String name) {
		return getTemplateContent("header", name);
	}

	/**
	 * Includes the header.jsp template file from your current theme's
	 * directory. if a name is specified then a specialised header
	 * header-{name}.jsp will be included.
	 * 
	 * @return header
	 */
	public static String getHeader() {
		return getTemplateContent("header");
	}

	/**
	 * Includes the footer.jsp template file from your current theme's
	 * directory. if a name is specified then a specialised footer
	 * footer-{name}.jsp will be included.
	 * 
	 * @return footer
	 */
	public static String getFooter() {
		return getTemplateContent("footer");
	}

	/**
	 * Includes the footer.jsp template file from your current theme's
	 * directory. if a name is specified then a specialised footer
	 * footer-{name}.jsp will be included.
	 * 
	 * @param name
	 *            footer-{name}.jsp
	 * @return footer
	 */
	public static String getFooter(String name) {
		return getTemplateContent("footer", name);
	}

	/**
	 * Includes the sidebar.jsp template file from your current theme's
	 * directory. if a name is specified then a specialised sidebar
	 * sidebar-{name}.jsp will be included.
	 * 
	 * @return sidebar
	 */
	public static String getSidebar() {
		return getTemplateContent("sidebar");
	}

	/**
	 * Includes the sidebar.jsp template file from your current theme's
	 * directory. if a name is specified then a specialised sidebar
	 * sidebar-{name}.jsp will be included.
	 * 
	 * @param name
	 *            sidebar-{name}.jsp
	 * @return sidebar
	 */
	public static String getSidebar(String name) {
		return getTemplateContent("sidebar", name);
	}

	/**
	 * Loads the comment template. For use in single post and Page displays.
	 */
	public static String commentTemplate() {
		return getTemplateContent("comments");
	}

	/**
	 * Loads the comment template. For use in single post and Page displays.
	 * 
	 * @param file
	 *            (optional) The file to load . Default: /comments.jsp example :
	 *            commentTemplate("/short-comments.jsp");
	 */
	public static String commentTemplate(String file) {
		return getTemplateContent("comments", file);
	}

	/**
	 * This method can return the content of type.jsp.
	 * 
	 * @param type
	 *            would be "header","footer","sidebar","comment"
	 * @return type.jsp
	 */
	private static String getTemplateContent(String type) {
		WrapperResponse wrapperResponse = new WrapperResponse(response);
		String path = "";
		if (type.equals("header")) {
			path = GeneralTemplate.getHeader();
		} else if (type.equals("footer")) {
			path = GeneralTemplate.getFooter();
		} else if (type.equals("sidebar")) {
			path = GeneralTemplate.getSidebar();
		} else if (type.equals("comments")) {
			path = GeneralTemplate.getCommentTemplate();
		}
		try {
			request.getRequestDispatcher(path)
					.include(request, wrapperResponse);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return wrapperResponse.getContent();
	}

	/**
	 * This method can return the content of a type-param.jsp.If it's not
	 * exist,return type.jsp
	 * 
	 * @param type
	 *            would be "header","footer","sidebar","comment"
	 * @param param
	 * @return type-param.jsp
	 */
	private static String getTemplateContent(String type, String param) {
		WrapperResponse wrapperResponse = new WrapperResponse(response);
		String path = "";
		if (type.equals("header")) {
			path = GeneralTemplate.getHeader(param);
		} else if (type.equals("footer")) {
			path = GeneralTemplate.getFooter(param);
		} else if (type.equals("sidebar")) {
			path = GeneralTemplate.getSidebar(param);
		} else if (type.equals("comments")) {
			path = GeneralTemplate.getCommentTemplate(param);
		}

		try {
			request.getRequestDispatcher(path)
					.include(request, wrapperResponse);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return wrapperResponse.getContent();
	}

	/**
	 * Displays information about your blog, mostly gathered from the
	 * information you supply in your User Profile and General Options from the
	 * Bloginfo Administration panels. It can be used anywhere within a page
	 * template. This always prints a result to the browser. If you need the
	 * values for use in jsp, use getBloginfo().
	 * 
	 * @param param
	 *            param may be one of these: name = Testpilot description = Just
	 *            another Windblog blog admin_email = admin@example
	 * 
	 *            stylesheet_directory =
	 *            http://example/home/wp/wp-content/themes/child-theme
	 *            stylesheet_url =
	 *            http://Testpilot.appspot.com/content/themes/a_theme/style.css
	 * 
	 *            url = http://Testpilot.appspot.com
	 * 
	 *            charset = UTF-8 html_type = text/html language = en-US
	 *            text_direction = ltr version = 2.9.2
	 * 
	 */
	public static String bloginfo(String param) {
		if (param.equalsIgnoreCase("name")) {
			return Bloginfo.getName();
		} else if (param.equalsIgnoreCase("description")) {
			return Bloginfo.getDescription();
		} else if (param.equalsIgnoreCase("admin_email")) {
			return Bloginfo.getAdminEmail();
		} else if (param.equalsIgnoreCase("stylesheet_directory")) {
			return Bloginfo.getStylesheetDirectory();
		} else if (param.equalsIgnoreCase("stylesheet_url")) {
			return Bloginfo.getStylesheetURL();
		} else if (param.equalsIgnoreCase("url")) {
			return Bloginfo.getURL();
		} else if (param.equalsIgnoreCase("charset")) {
			return Bloginfo.getCharset();
		} else if (param.equalsIgnoreCase("html_type")) {
			return Bloginfo.getHtmlType();
		} else if (param.equalsIgnoreCase("language")) {
			return Bloginfo.getLanguage();
		} else if (param.equalsIgnoreCase("text_direction")) {
			return Bloginfo.getTextDirection();
		} else if (param.equalsIgnoreCase("version")) {
			return Bloginfo.getVersion();
		} else {
			throw new RuntimeException("bloginfo \"" + param
					+ "\"is not exist.");
		}
	}

	/**
	 * The get_bloginfo() function returns information about your blog which can
	 * then be used elsewhere in your PHP code. This function, as well as
	 * bloginfo(), can also be used to display your blog information.
	 * 
	 * @param param
	 *            param may be one of these: name = Testpilot description = Just
	 *            another Windblog blog admin_email = admin@example
	 * 
	 *            stylesheet_directory =
	 *            http://example/home/wp/wp-content/themes/child-theme
	 *            stylesheet_url =
	 *            http://Testpilot.appspot.com/content/themes/a_theme/style.css
	 * 
	 *            url = http://Testpilot.appspot.com
	 * 
	 *            charset = UTF-8 html_type = text/html language = en-US
	 *            text_direction = ltr version = 2.9.2
	 * 
	 */
	public static void getBloginfo(String param) {
		bloginfo(param);
	}

	/**
	 * This function displays a date-based archives list. This tag can be used
	 * anywhere within a template.
	 * 
	 * @param type
	 *            (string) The type of archive list to display. Defaults to
	 *            Windblog settings. Valid values: yearly monthly - Default
	 *            daily weekly
	 * @param limit
	 *            (integer) Number of archives to get. Default is no limit.
	 * @param format
	 *            (string) Format for the archive list. Valid values: html - In
	 *            HTML list (<li>) tags and before and after strings. This is
	 *            the default. option - In select (<select>) or dropdown option
	 *            (<option>) tags. link - Within link (<link>) tags. custom -
	 *            Custom list using the before and after strings.
	 * @param before
	 *            (string) Text to place before the link when using the html or
	 *            custom for format option. There is no default.
	 * @param after
	 *            (string) Text to place after the link when using the html or
	 *            custom for format option. There is no default.
	 * @param show_post_count
	 *            (Boolean) Display number of posts in an archive or do not. For
	 *            use with all type except 'postbypost'. Default is True.
	 */
	public static String getArchives(String param) {
		ParamParser pp = new ParamParser();

		pp.setParameter("type", "monthly");
		pp.setParameter("limit", -1);
		pp.setParameter("format", "html");
		pp.setParameter("before", "");
		pp.setParameter("after", "");
		pp.setParameter("showPostCount", "false");

		pp.parse(param);

		String type = (String) pp.getParameter("type");
		int limit = (Integer) pp.getParameter("limit");
		String format = (String) pp.getParameter("format");
		String before = (String) pp.getParameter("before");
		String after = (String) pp.getParameter("after");
		Boolean showPostCount = Boolean.parseBoolean((String) pp
				.getParameter("showPostCount"));

		if (limit == -1)
			limit = Integer.MAX_VALUE;

		int i = 0;

		if (format.equals("html")) {
			before = "<li>";
			after = "</li>";
		} else if (format.equals("option")) {
			before = "<option>";
			after = "</option>";
		}

		StringWriter sw = new StringWriter();
		PrintWriter writer = new PrintWriter(sw);
		for (Archive a : ArchiveService.getArchives(type)) {

			if ((i++) < limit) {
				writer.print(before);
				writer.print("<a href=\"" + a.getPermalink() + "\">"
						+ a.getName() + "</a></li>");

				if (showPostCount)
					writer.print("(" + a.getCount() + ")");

				writer.print(after);
				writer.println();

			}

		}
		return sw.getBuffer().toString();
	}

	/**
	 * Displays a list of Categories as links. When a Category link is clicked,
	 * all the posts in that Category will display on a Category Page using the
	 * appropriate Category Template.
	 * 
	 * @param style
	 *            (string) Style to display the categories list in. A value of
	 *            list displays the categories as list items while none
	 *            generates no special display method (the list items are
	 *            separated by br tags). The default setting is list (creates list items for an
	 *            unordered list). Valid values: list(Default) , none .
	 * @param showCount
	 *            (boolean) Toggles the display of the current count of posts 
	 *            in each category. The default is false (do not show post counts).
	 *            Valid values: true , false(Default).
	 * @param hideEmpty
	 *            (boolean) Toggles the display of categories with no posts. 
	 *            The default is true (hide empty categories).
	 *            Valid values: true(Default) , false.
	 * @param use_desc_for_title
	 *            (boolean) Sets whether a category's description is inserted into the 
	 *            title attribute of the links created (i.e. <a title="<em>Category 
	 *            Description</em>" href="...). The default is true (category descriptions 
	 *            will be inserted). 
	 *            Valid values: true(Default) , false.
	 * @param number 
	 *            (integer) Sets the number of Categories to display. Default is -1 to no LIMIT.
	 * @param title 
	 *            (string) Set the title and style of the outer list item. Defaults to "Categories".
	 */
	public static String listCategories(String param) {
		ParamParser pp = new ParamParser();

		pp.setParameter("style", "list");
		pp.setParameter("showCount", false);
		pp.setParameter("hideEmpty", true);
		pp.setParameter("useDescForTitle", true);
		pp.setParameter("number", -1);
		pp.setParameter("title","<h2>Categories</h2>");
		pp.parse(param);

		String style = (String) pp.getParameter("style");
		String title = (String) pp.getParameter("title");
		Boolean showCount = Boolean.parseBoolean(pp
				.getParameter("showCount").toString());
		Boolean hideEmpty = Boolean.parseBoolean(pp
				.getParameter("hideEmpty").toString());
		Boolean useDescForTitle = Boolean.parseBoolean(pp
				.getParameter("useDescForTitle").toString());

		int number = (Integer) pp.getParameter("number");

		StringWriter sw = new StringWriter();
		PrintWriter writer = new PrintWriter(sw);
		
		writer.println("<li>"+title);
		writer.println("<ul>");

		
		for(Category c : CategoryService.getCategories())
		{
			if(!(hideEmpty && c.getCount()==0)||number>0)
			{
				String desc = "";
				if(useDescForTitle)
					desc = "title=\"<em>"+c.getDescription()+"</em>\" ";
			
				String link = "<a "+desc+"href=" + c.getArchiveLink() + ">"+c.getName() +"</a>";
			
				if(showCount)
					link += "("+c.getCount() +")";
			
				if(style.equals("list"))
					link = "<li>"+link+"</li>";
				else if(style.equals("none"))
					link = link+"<br/>";
				
				writer.println(link);
				
				number--;
			}
		}
		writer.println("</ul>");
		writer.println("</li>");
		return sw.getBuffer().toString();
	}
	
	/**
	 * Displays a list of WordPress Pages as links. It is often used to customize the Sidebar or Header, 
	 * but may be used in other Templates as well. 
	 * 
	 * @param depth 
	 *            (integer) This parameter controls how many levels in the hierarchy of pages 
	 *            are to be included in the list generated by wp_list_pages. The default value 
	 *            is 0 (display all pages, including all sub-pages). 
	 *              0      (default) Displays pages at any depth and arranges them hierarchically in nested lists 
	 *             -1      Displays pages at any depth and arranges them in a single, flat list 
	 *              1      Displays top-level Pages only 
	 *              2, 3 … Displays Pages to the given depth 

	 * @param linkBefore 
	 *            (string) Sets the text or html that precedes the link text inside <a> tag. 
	 * @param linkAfter 
	 *            (string) Sets the text or html that follows the link text inside <a> tag. 
	 * @param title 
	 *            (string) Set the text and style of the Page list's heading.
	 *            
	 */
	public static String listPages(String param)
	{
		ParamParser pp = new ParamParser();

		pp.setParameter("depth", 0);
		pp.setParameter("linkBefore", "");
		pp.setParameter("linkAfter", "");
		pp.setParameter("title", "<h2>Pages</h2>");
		
		pp.parse(param);

		int depth = Integer.parseInt(pp.getParameter("depth").toString());
		String linkBefore = (String) pp.getParameter("linkBefore");
		String linkAfter = (String) pp.getParameter("linkAfter");
		String title = (String) pp.getParameter("title");
		
		StringWriter sw = new StringWriter();
		PrintWriter writer = new PrintWriter(sw);
		
		writer.println("<li>"+title);
		writer.println("<ul>");
			

		Page root = PageService.getRoot(); 					
		Stack<Page> pageStack = new Stack<Page>(); 					
		Stack<Integer> levelStack = new Stack<Integer>(); 					
		int curLevel = -1; 					
		int preLevel = 0; 					
		pageStack.push(root); 					
		levelStack.push(curLevel); 
		if(root!=null )
		{
			if(depth==0){
				while(!pageStack.empty())
				{
					Page currentPage = pageStack.pop();
					curLevel = levelStack.pop();
					List<Page> childPages = new LinkedList<Page>(currentPage.getChildPages());
					if(!currentPage.isRoot()){
						int endnum = preLevel-curLevel;
						while((endnum--)>0)writer.println("</ul></li>");
						writer.println("<li class=\"level-"+curLevel +"\"><a href=\""+linkBefore+currentPage.getPermalink()+linkAfter +"\">"+currentPage.getTitle() +"</a>" + (childPages.size()>0?"<ul>":"</li>"));											
					}
					preLevel = curLevel;
					Collections.sort(childPages);
					for(Page p : childPages){
						pageStack.push(p);
						levelStack.push(curLevel+1);
					}
				}
				int endnum = preLevel-0;
				while(endnum>0){
					writer.println("</ul></li>");
					endnum--;
				}
			}else if(depth==-1){
				while(!pageStack.empty())
				{
					Page currentPage = pageStack.pop();
					curLevel = levelStack.pop();
					List<Page> childPages = new LinkedList<Page>(currentPage.getChildPages());
					if(!currentPage.isRoot()){
					writer.println("<li class=\"level-1\"><a href=\""+linkBefore+currentPage.getPermalink()+linkAfter +"\">"+currentPage.getTitle() +"</a>" + "</li>");											
					}
					preLevel = curLevel;
					Collections.sort(childPages);
					for(Page p : childPages){
						pageStack.push(p);
						levelStack.push(curLevel+1);
					}
					
				}
			}else if(depth==1)
			{
				Page rootPage = pageStack.pop();
				pageStack.addAll(rootPage.getChildPages());
				while(!pageStack.empty())
				{
					Page currentPage = pageStack.pop();
					
					if(!currentPage.isRoot()){
						writer.println("<li class=\"level-1\"><a href=\""+linkBefore+currentPage.getPermalink()+linkAfter +"\">"+currentPage.getTitle() +"</a>" + "</li>");											
					}
					
				}
			}else{
				while(!pageStack.empty())
				{
					Page currentPage = pageStack.pop();
					curLevel = levelStack.pop();
					List<Page> childPages = new LinkedList<Page>(currentPage.getChildPages());
					if(!currentPage.isRoot()){
					int endnum = preLevel-curLevel;
					while((endnum--)>0)writer.println("</ul></li>");
					writer.println("<li class=\"level-"+curLevel +"\"><a href=\""+linkBefore+currentPage.getPermalink()+linkAfter +"\">"+currentPage.getTitle() +"</a>" + (childPages.size()>0?"<ul>":"</li>"));											
					}
					preLevel = curLevel;
					Collections.sort(childPages);
					if(curLevel+1<=depth)
						for(Page p : childPages){
							pageStack.push(p);
							levelStack.push(curLevel+1);
						}
				}
				int endnum = preLevel-0;
				while(endnum>0){
					writer.println("</ul></li>");
					endnum--;
				}
			}
		}//end if
		
		writer.println("</ul>");
		writer.println("</li>");
		
		return sw.getBuffer().toString();
	}
	
	
	
	public static String listBookmarks(String param)
	{
		ParamParser pp = new ParamParser();

		pp.setParameter("limit", -1);
		pp.setParameter("title", "<h2>Blogroll</h2>");

		pp.parse(param);

		int limit = Integer.parseInt(pp.getParameter("limit").toString());
		String title = (String) pp.getParameter("title");
		
		if (limit == -1)
			limit = Integer.MAX_VALUE;
		
		StringWriter sw = new StringWriter();
		PrintWriter writer = new PrintWriter(sw);
		
		writer.println("<li>"+title);
		writer.println("<ul>");
		
		int i=0;
		for(Link l : LinkService.getLinks())
		{
			if(i<limit)
				writer.println("<li><a href=\""+l.getUrl()+"\" >"+l.getName()+"</a></li>");
		}
		
		writer.println("</ul>");
		writer.println("</li>");
	
		
	    return sw.getBuffer().toString();
	}

	
	
	
	//Post tags
	private static Post currentPost;
	private static Stack<Post> posts;
	/**
	 * init for post tags.
	 */
	public static void setPosts(List<Post> paramPosts) {
		Stack<Post> tmp = new Stack<Post>();
		posts = new Stack<Post>(); 
		for(Post p : paramPosts){
			tmp.push(p);
		}
		
		while(!tmp.empty())
		{
			posts.push(tmp.pop());
		}
		
		currentPost = null;
	}

	/**
	 * Returns true if there is a post available in the current query. 
	 * Usually used at the beginning of the loop. 
	 */
	public static Boolean havePosts()
	{
		return !posts.empty();
	}
	
	/**
	 * The method thePost() takes the current item in the collection of 
	 * posts and makes it available for use inside this iteration of The Loop. 
	 * Without the_post(),many of the Template Tags used in your theme would not work. 
	 */
	public static void thePost()
	{
		currentPost = posts.pop();
	}
	
	/**
	 * Used on single post permalink pages, this method displays a 
	 * link to the previous post which exists in chronological order from 
	 * the current post. 
	 * This tag must be used in The Loop.
	 */
	public static String previousPostLink()
	{
		return currentPost.getPreviousPostLink();
	}
	
	/**
	 * Used on single post permalink pages, this method displays a 
	 * link to the previous post which exists in chronological order from 
	 * the current post. 
	 * This tag must be used in The Loop.
	 * @param format
	 * 				format usually like "prefix %link suffix". The function will
	 *              replace the "%link" by posts permalink.
	 */
	public static String previousPostLink(String format)
	{
		return currentPost.getPreviousPostLink(format);
	}
	
	/**
	 * Used on single post permalink pages, this method displays a 
	 * link to the next post which exists in chronological order from 
	 * the current post. 
	 * This tag must be used in The Loop.
	 */
	public static String nextPostLink()
	{
		return currentPost.getNextPostLink();
	}
	
	/**
	 * Used on single post permalink pages, this method displays a 
	 * link to the next post which exists in chronological order from 
	 * the current post. 
	 * This tag must be used in The Loop.
	 * @param format
	 * 				format usually like "prefix %link suffix". The function will
	 *              replace the "%link" by posts permalink.
	 */
	public static String nextPostLink(String format)
	{
		return currentPost.getNextPostLink(format);
	}
	
	/**
	 * The post_class template tag creates CSS selectors to style content only 
	 * within the post content area. 
	 */
	public static String postClass()
	{
		return "class=\"post\"";
	}
	
	/**
	 * Displays a link to the category or categories a post belongs to. 
	 * This tag must be used within The Loop. 
	 * @param separator
	 * 				(string) Text or character to display between each category link. 
	 * 				The default is to place the links in an unordered list. 
	 */
	public static String theCategory(String separator)
	{
		List<Category> cats = currentPost.getCategories();
		StringBuilder sb = new StringBuilder();
		for(Category cat : cats)
		{
			sb.append("<a href=\""+cat.getArchiveLink()+"\">"+cat.getName()+"</a>");
			if(cats.indexOf(cat)!=cats.size()-1)
				sb.append(separator);
		}
		return sb.toString();
	}
	
	/**
	 * Displays the numeric ID of the current post. This tag must be within The Loop. 
	 */
	public static long theID()
	{
		return currentPost.getKey().getId();
	}
	
	/**
	 * Displays the URL for the permalink to the post currently being processed in The Loop. 
	 * This tag must be within The Loop
	 */
	public static String thePermalink()
	{
		return currentPost.getPermalink();
	}
	
	/**
	 * Displays or returns the title of the current post. 
	 * This tag must be within The Loop.
	 * @return
	 */
	public static String theTitle()
	{
		return currentPost.getTitle();
	}
	
	/**
	 * Displays the time of the current post. 
	 * This tag must be used within The Loop. 
	 * @param format
	 * 				(string) (optional) The format the time is to display in. 
	 * 				Defaults to the time format configured in your Windblog options. 
	 * 				Default: None 
	 */
	public static String theTime(String format)
	{
		return currentPost.getFormatedDate();
	}
	
	/**
	 * Displays the time of the current post. 
	 * This tag must be used within The Loop. 
	 * Default invoke.
	 */
	public static String theTime()
	{
		return currentPost.getFormatedDate();
	}
	
	/**
	 * The author of a post can be displayed by using this Template Tag. 
	 * This tag must be used within The Loop. 
	 */
	public static String theAuthor()
	{
		return currentPost.getAuthor();
	}
	
	/**
	 * Displays the contents of the current post. 
	 * This tag must be within The loop. 
	 */
	public static String theContent()
	{
		return currentPost.getContent();
	}
	
	//Edit Link Tags
	/**
	 * Displays a link to edit the current post, if a user is logged in and allowed to edit the post. 
	 * It must be within The Loop. 
	 * @param link
	 * 			  (string) (optional) The link text. 
	 * 			   Default: "Edit This" 
	 */
	public static String editPostLink(String link)
	{
		return "<a href=\""+currentPost.getEditLink()+"\">"+link+"</a>";
	}
	/**
	 * Displays a link to edit the current post, if a user is logged in and allowed to edit the post. 
	 * It must be within The Loop. 
	 */
	public static String editPostLink()
	{
		return editPostLink("Edit This");
	}
	
	//Comment tags  
	/**
	 *  it displays a normal link to comments. This tag must be within The Loop
	 *  @param zero
	 *             (string) Text to display when there are no comments. Defaults to 'No Comments'. 
	 *  @param one
	 *             (string) Text to display when there is one comment. Defaults to '1 Comment'. 
	 *  @param more 
	 *             (string) Text to display when there are more than one comments. '%' is replaced 
	 *             by the number of comments, so '% so far' is displayed as "5 so far" when there 
	 *             are five comments. Defaults to '% Comments'. 
	 */
	public static String commentsPopupLink(String zero,String one,String more)
	{
		String text = "";
		Integer commentsNum = currentPost.getComments().size();
		switch(commentsNum)
		{
		case 0 : text = zero; break;
		case 1 : text = one ; break;
		default: text = more.replace("%",commentsNum.toString()); break;
		}
		return "<a href=\""+currentPost.getPermalink()+"#comments\">"+text+"</a>";
	}
	
	/**
	 * Default invoke of CommentsPopupLink.
	 */
	public static String commentsPopupLink()
	{
		return commentsPopupLink("No Comments","1 Comment","% Comments");
	}
	
	/**
	 * Displays or returns the category title for the current page. 
	 * Can be used only outside The Loop. 
	 */
	public static String singleCatTitle()
	{
		return WBQuery.getTitle();
	}
}